/* Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.activiti.engine.impl.bpmn.behavior;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

import org.activiti.engine.ActivitiException;
import org.activiti.engine.ActivitiIllegalArgumentException;
import org.activiti.engine.delegate.BpmnError;
import org.activiti.engine.impl.bpmn.helper.ErrorPropagation;
import org.activiti.engine.impl.bpmn.helper.ScopeUtil;
import org.activiti.engine.impl.context.Context;
import org.activiti.engine.impl.delegate.ExecutionListenerInvocation;
import org.activiti.engine.impl.history.handler.ActivityInstanceStartHandler;
import org.activiti.engine.impl.persistence.entity.ExecutionEntity;
import org.activiti.engine.impl.pvm.delegate.ActivityExecution;
import org.activiti.engine.impl.pvm.delegate.CompositeActivityBehavior;
import org.activiti.engine.impl.pvm.delegate.SubProcessActivityBehavior;
import org.activiti.engine.impl.pvm.process.ActivityImpl;
import org.activiti.engine.impl.pvm.runtime.AtomicOperation;
import org.activiti.engine.impl.pvm.runtime.InterpretableExecution;
import org.flowable.common.engine.api.delegate.Expression;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.delegate.ExecutionListener;
import org.flowable.engine.impl.delegate.ActivityBehavior;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Implementation of the multi-instance functionality as described in the BPMN 2.0 spec.
 * <p>
 * Multi instance functionality is implemented as an {@link ActivityBehavior} that wraps the original {@link ActivityBehavior} of the activity.
 * <p>
 * Only subclasses of {@link AbstractBpmnActivityBehavior} can have multi-instance behavior. As such, special logic is contained in the {@link AbstractBpmnActivityBehavior} to delegate to the
 * {@link MultiInstanceActivityBehavior} if needed.
 *
 * @author Joram Barrez
 * @author Falko Menge
 */
public abstract class MultiInstanceActivityBehavior extends FlowNodeActivityBehavior
        implements CompositeActivityBehavior, SubProcessActivityBehavior {

    protected static final Logger LOGGER = LoggerFactory.getLogger(MultiInstanceActivityBehavior.class);

    // Variable names for outer instance(as described in spec)
    protected final String NUMBER_OF_INSTANCES = "nrOfInstances";
    protected final String NUMBER_OF_ACTIVE_INSTANCES = "nrOfActiveInstances";
    protected final String NUMBER_OF_COMPLETED_INSTANCES = "nrOfCompletedInstances";

    // Instance members
    protected ActivityImpl activity;
    protected AbstractBpmnActivityBehavior innerActivityBehavior;
    protected Expression loopCardinalityExpression;
    protected Expression completionConditionExpression;
    protected Expression collectionExpression;
    protected String collectionVariable;
    protected String collectionElementVariable;
    // default variable name for loop counter for inner instances (as described in the spec)
    protected String collectionElementIndexVariable = "loopCounter";

    /**
     * @param activity
     * @param innerActivityBehavior The original {@link ActivityBehavior} of the activity that will be wrapped inside this behavior.
     */
    public MultiInstanceActivityBehavior(ActivityImpl activity, AbstractBpmnActivityBehavior innerActivityBehavior) {
        this.activity = activity;
        setInnerActivityBehavior(innerActivityBehavior);
    }

    @Override
    public void execute(DelegateExecution execution) {
        ActivityExecution activityExecution = (ActivityExecution) execution;
        if (getLocalLoopVariable(activityExecution, getCollectionElementIndexVariable()) == null) {
            try {
                createInstances(activityExecution);
            } catch (BpmnError error) {
                ErrorPropagation.propagateError(error, activityExecution);
            }

            if (resolveNrOfInstances(activityExecution) == 0) {
                leave(activityExecution);
            }
        } else {
            innerActivityBehavior.execute(execution);
        }
    }

    protected abstract void createInstances(ActivityExecution execution);

    // Intercepts signals, and delegates it to the wrapped {@link ActivityBehavior}.
    @Override
    public void signal(ActivityExecution execution, String signalName, Object signalData) throws Exception {
        innerActivityBehavior.signal(execution, signalName, signalData);
    }

    // required for supporting embedded subprocesses
    @Override
    public void lastExecutionEnded(ActivityExecution execution) {
        ScopeUtil.createEventScopeExecution((ExecutionEntity) execution);
        leave(execution);
    }

    // required for supporting external subprocesses
    @Override
    public void completing(DelegateExecution execution, DelegateExecution subProcessInstance) throws Exception {
    }

    // required for supporting external subprocesses
    @Override
    public void completed(ActivityExecution execution) throws Exception {
        leave(execution);
    }

    // Helpers //////////////////////////////////////////////////////////////////////

    @SuppressWarnings("rawtypes")
    protected int resolveNrOfInstances(ActivityExecution execution) {
        int nrOfInstances = -1;
        if (loopCardinalityExpression != null) {
            nrOfInstances = resolveLoopCardinality(execution);
        } else if (collectionExpression != null) {
            Object obj = collectionExpression.getValue(execution);
            if (!(obj instanceof Collection)) {
                throw new ActivitiIllegalArgumentException(collectionExpression.getExpressionText() + "' didn't resolve to a Collection");
            }
            nrOfInstances = ((Collection) obj).size();
        } else if (collectionVariable != null) {
            Object obj = execution.getVariable(collectionVariable);
            if (obj == null) {
                throw new ActivitiIllegalArgumentException("Variable " + collectionVariable + " was not found");
            }
            if (!(obj instanceof Collection)) {
                throw new ActivitiIllegalArgumentException("Variable " + collectionVariable + "' is not a Collection");
            }
            nrOfInstances = ((Collection) obj).size();
        } else {
            throw new ActivitiIllegalArgumentException("Couldn't resolve collection expression nor variable reference");
        }
        return nrOfInstances;
    }

    @SuppressWarnings("rawtypes")
    protected void executeOriginalBehavior(ActivityExecution execution, int loopCounter) {
        if (usesCollection() && collectionElementVariable != null) {
            Collection collection = null;
            if (collectionExpression != null) {
                collection = (Collection) collectionExpression.getValue(execution);
            } else if (collectionVariable != null) {
                collection = (Collection) execution.getVariable(collectionVariable);
            }

            Object value = null;
            int index = 0;
            Iterator it = collection.iterator();
            while (index <= loopCounter) {
                value = it.next();
                index++;
            }
            setLoopVariable(execution, collectionElementVariable, value);
        }

        // If loopcounter == 1, then historic activity instance already created, no need to
        // pass through executeActivity again since it will create a new historic activity
        if (loopCounter == 0) {
            callCustomActivityStartListeners(execution);
            innerActivityBehavior.execute(execution);
        } else {
            execution.executeActivity(activity);
        }
    }

    protected boolean usesCollection() {
        return collectionExpression != null
                || collectionVariable != null;
    }

    protected boolean isExtraScopeNeeded() {
        // special care is needed when the behavior is an embedded subprocess (not very clean, but it works)
        return innerActivityBehavior instanceof org.activiti.engine.impl.bpmn.behavior.SubProcessActivityBehavior;
    }

    protected int resolveLoopCardinality(ActivityExecution execution) {
        // Using Number since expr can evaluate to eg. Long (which is also the default for Juel)
        Object value = loopCardinalityExpression.getValue(execution);
        if (value instanceof Number) {
            return ((Number) value).intValue();
        } else if (value instanceof String) {
            return Integer.valueOf((String) value);
        } else {
            throw new ActivitiIllegalArgumentException("Could not resolve loopCardinality expression '"
                    + loopCardinalityExpression.getExpressionText() + "': not a number nor number String");
        }
    }

    protected boolean completionConditionSatisfied(ActivityExecution execution) {
        if (completionConditionExpression != null) {
            Object value = completionConditionExpression.getValue(execution);
            if (!(value instanceof Boolean)) {
                throw new ActivitiIllegalArgumentException("completionCondition '"
                        + completionConditionExpression.getExpressionText()
                        + "' does not evaluate to a boolean value");
            }
            Boolean booleanValue = (Boolean) value;
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Completion condition of multi-instance satisfied: {}", booleanValue);
            }
            return booleanValue;
        }
        return false;
    }

    protected void setLoopVariable(ActivityExecution execution, String variableName, Object value) {
        execution.setVariableLocal(variableName, value);
    }

    protected Integer getLoopVariable(ActivityExecution execution, String variableName) {
        Object value = execution.getVariableLocal(variableName);
        ActivityExecution parent = execution.getParent();
        while (value == null && parent != null) {
            value = parent.getVariableLocal(variableName);
            parent = parent.getParent();
        }
        return (Integer) (value != null ? value : 0);
    }

    protected Integer getLocalLoopVariable(ActivityExecution execution, String variableName) {
        return (Integer) execution.getVariableLocal(variableName);
    }

    /**
     * Since the first loop of the multi instance is not executed as a regular activity, it is needed to call the start listeners yourself.
     */
    protected void callCustomActivityStartListeners(ActivityExecution execution) {
        List<ExecutionListener> listeners = activity.getExecutionListeners(org.activiti.engine.impl.pvm.PvmEvent.EVENTNAME_START);

        if (listeners != null) {
            List<ExecutionListener> filteredExecutionListeners = new ArrayList<>(listeners.size());
            // Sad that we have to do this, but it's the only way I could find (which is also safe for backwards compatibility)

            for (ExecutionListener executionListener : listeners) {
                if (!(executionListener instanceof ActivityInstanceStartHandler)) {
                    filteredExecutionListeners.add(executionListener);
                }
            }

            CallActivityListenersOperation atomicOperation = new CallActivityListenersOperation(filteredExecutionListeners);
            Context.getCommandContext().performOperation(atomicOperation, (InterpretableExecution) execution);
        }

    }

    /**
     * Since no transitions are followed when leaving the inner activity, it is needed to call the end listeners yourself.
     */
    protected void callActivityEndListeners(ActivityExecution execution) {
        List<ExecutionListener> listeners = activity.getExecutionListeners(org.activiti.engine.impl.pvm.PvmEvent.EVENTNAME_END);
        CallActivityListenersOperation atomicOperation = new CallActivityListenersOperation(listeners);
        Context.getCommandContext().performOperation(atomicOperation, (InterpretableExecution) execution);
    }

    protected void logLoopDetails(ActivityExecution execution, String custom, int loopCounter,
                                  int nrOfCompletedInstances, int nrOfActiveInstances, int nrOfInstances) {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Multi-instance '{}' {}. Details: loopCounter={}, nrOrCompletedInstances={},nrOfActiveInstances={},nrOfInstances={}",
                    execution.getActivity(), custom, loopCounter, nrOfCompletedInstances, nrOfActiveInstances, nrOfInstances);
        }
    }

    // Getters and Setters ///////////////////////////////////////////////////////////

    public Expression getLoopCardinalityExpression() {
        return loopCardinalityExpression;
    }

    public void setLoopCardinalityExpression(Expression loopCardinalityExpression) {
        this.loopCardinalityExpression = loopCardinalityExpression;
    }

    public Expression getCompletionConditionExpression() {
        return completionConditionExpression;
    }

    public void setCompletionConditionExpression(Expression completionConditionExpression) {
        this.completionConditionExpression = completionConditionExpression;
    }

    public Expression getCollectionExpression() {
        return collectionExpression;
    }

    public void setCollectionExpression(Expression collectionExpression) {
        this.collectionExpression = collectionExpression;
    }

    public String getCollectionVariable() {
        return collectionVariable;
    }

    public void setCollectionVariable(String collectionVariable) {
        this.collectionVariable = collectionVariable;
    }

    public String getCollectionElementVariable() {
        return collectionElementVariable;
    }

    public void setCollectionElementVariable(String collectionElementVariable) {
        this.collectionElementVariable = collectionElementVariable;
    }

    public String getCollectionElementIndexVariable() {
        return collectionElementIndexVariable;
    }

    public void setCollectionElementIndexVariable(String collectionElementIndexVariable) {
        this.collectionElementIndexVariable = collectionElementIndexVariable;
    }

    public void setInnerActivityBehavior(AbstractBpmnActivityBehavior innerActivityBehavior) {
        this.innerActivityBehavior = innerActivityBehavior;
        this.innerActivityBehavior.setMultiInstanceActivityBehavior(this);
    }

    public AbstractBpmnActivityBehavior getInnerActivityBehavior() {
        return innerActivityBehavior;
    }

    /**
     * ACT-1339. Calling ActivityEndListeners within an {@link AtomicOperation} so that an executionContext is present.
     *
     * @author Aris Tzoumas
     * @author Joram Barrez
     */
    private static final class CallActivityListenersOperation implements AtomicOperation {

        private List<ExecutionListener> listeners;

        private CallActivityListenersOperation(List<ExecutionListener> listeners) {
            this.listeners = listeners;
        }

        @Override
        public void execute(InterpretableExecution execution) {
            for (ExecutionListener executionListener : listeners) {
                try {
                    Context.getProcessEngineConfiguration()
                            .getDelegateInterceptor()
                            .handleInvocation(new ExecutionListenerInvocation(executionListener, execution));
                } catch (Exception e) {
                    throw new ActivitiException("Couldn't execute listener", e);
                }
            }
        }

        @Override
        public boolean isAsync(InterpretableExecution execution) {
            return false;
        }

    }
}
